"""
Dynamic call detection for Python code.

Detects dynamic function calls that cannot be statically analyzed,
including getattr, eval, exec, and other dynamic invocation patterns.
"""

import ast
from typing import Dict, List, Any, Optional, Set
from collections import defaultdict

from ...core.error_handler import AnalysisError, log_info


class DynamicCallDetector:
    """Detects dynamic function calls in Python code."""
    
    def __init__(self):
        """Initialize the dynamic call detector."""
        self.dynamic_patterns = self._initialize_dynamic_patterns()
        self.risk_levels = {
            'low': ['getattr', 'hasattr', 'setattr', 'delattr'],
            'medium': ['__import__', 'importlib.import_module', 'globals', 'locals', 'vars'],
            'high': ['eval', 'exec', 'compile']
        }
    
    def _initialize_dynamic_patterns(self) -> Dict[str, Dict[str, Any]]:
        """Initialize patterns for detecting dynamic calls."""
        return {
            'getattr': {
                'description': 'Dynamic attribute access',
                'risk_level': 'low',
                'can_execute': True,
                'pattern': 'getattr(obj, name, default=None)',
                'analysis_difficulty': 'medium'
            },
            'hasattr': {
                'description': 'Check for attribute existence',
                'risk_level': 'low',
                'can_execute': False,
                'pattern': 'hasattr(obj, name)',
                'analysis_difficulty': 'low'
            },
            'setattr': {
                'description': 'Dynamic attribute assignment',
                'risk_level': 'low',
                'can_execute': False,
                'pattern': 'setattr(obj, name, value)',
                'analysis_difficulty': 'low'
            },
            'delattr': {
                'description': 'Dynamic attribute deletion',
                'risk_level': 'low',
                'can_execute': False,
                'pattern': 'delattr(obj, name)',
                'analysis_difficulty': 'low'
            },
            'eval': {
                'description': 'Evaluate Python expression',
                'risk_level': 'high',
                'can_execute': True,
                'pattern': 'eval(expression, globals=None, locals=None)',
                'analysis_difficulty': 'impossible'
            },
            'exec': {
                'description': 'Execute Python code',
                'risk_level': 'high',
                'can_execute': True,
                'pattern': 'exec(code, globals=None, locals=None)',
                'analysis_difficulty': 'impossible'
            },
            'compile': {
                'description': 'Compile Python code',
                'risk_level': 'high',
                'can_execute': True,
                'pattern': 'compile(source, filename, mode)',
                'analysis_difficulty': 'high'
            },
            '__import__': {
                'description': 'Dynamic module import',
                'risk_level': 'medium',
                'can_execute': True,
                'pattern': '__import__(name, globals, locals, fromlist, level)',
                'analysis_difficulty': 'high'
            },
            'globals': {
                'description': 'Access global namespace',
                'risk_level': 'medium',
                'can_execute': False,
                'pattern': 'globals()',
                'analysis_difficulty': 'medium'
            },
            'locals': {
                'description': 'Access local namespace',
                'risk_level': 'medium',
                'can_execute': False,
                'pattern': 'locals()',
                'analysis_difficulty': 'medium'
            },
            'vars': {
                'description': 'Get object variables',
                'risk_level': 'medium',
                'can_execute': False,
                'pattern': 'vars(obj=None)',
                'analysis_difficulty': 'medium'
            }
        }
    
    def detect_dynamic_calls(self, ast_tree: ast.AST) -> Dict[str, Any]:
        """Detect dynamic calls in the AST."""
        dynamic_info = {
            'dynamic_calls': [],
            'risk_assessment': {
                'low_risk': [],
                'medium_risk': [],
                'high_risk': []
            },
            'patterns': {
                'attribute_access': [],
                'code_execution': [],
                'module_import': [],
                'namespace_access': []
            },
            'statistics': {
                'total_dynamic_calls': 0,
                'unique_patterns': 0,
                'high_risk_count': 0,
                'functions_with_dynamic_calls': set()
            }
        }
        
        visitor = DynamicCallVisitor(self.dynamic_patterns)
        visitor.visit(ast_tree)
        
        # Process detected calls
        for call in visitor.dynamic_calls:
            dynamic_info['dynamic_calls'].append(call)
            
            # Categorize by risk level
            risk_level = call.get('risk_level', 'unknown')
            if risk_level in ['low', 'medium', 'high']:
                dynamic_info['risk_assessment'][f'{risk_level}_risk'].append(call)
            
            # Categorize by pattern type
            call_type = call.get('call_type')
            if call_type == 'attribute_access':
                dynamic_info['patterns']['attribute_access'].append(call)
            elif call_type == 'code_execution':
                dynamic_info['patterns']['code_execution'].append(call)
            elif call_type == 'module_import':
                dynamic_info['patterns']['module_import'].append(call)
            elif call_type == 'namespace_access':
                dynamic_info['patterns']['namespace_access'].append(call)
            
            # Track function
            if call.get('function'):
                dynamic_info['statistics']['functions_with_dynamic_calls'].add(call['function'])
        
        # Calculate statistics
        dynamic_info['statistics']['total_dynamic_calls'] = len(dynamic_info['dynamic_calls'])
        dynamic_info['statistics']['unique_patterns'] = len(set(
            call['pattern'] for call in dynamic_info['dynamic_calls']
        ))
        dynamic_info['statistics']['high_risk_count'] = len(
            dynamic_info['risk_assessment']['high_risk']
        )
        dynamic_info['statistics']['functions_with_dynamic_calls'] = list(
            dynamic_info['statistics']['functions_with_dynamic_calls']
        )
        
        # Add analysis recommendations
        dynamic_info['recommendations'] = self._generate_recommendations(dynamic_info)
        
        return dynamic_info
    
    def _generate_recommendations(self, dynamic_info: Dict[str, Any]) -> List[Dict[str, Any]]:
        """Generate recommendations for handling dynamic calls."""
        recommendations = []
        
        high_risk_calls = dynamic_info['risk_assessment']['high_risk']
        if high_risk_calls:
            recommendations.append({
                'type': 'security',
                'priority': 'high',
                'message': f"Found {len(high_risk_calls)} high-risk dynamic calls (eval, exec, compile). "
                          "Consider using safer alternatives or adding input validation.",
                'affected_functions': list(set(call.get('function', 'unknown') for call in high_risk_calls))
            })
        
        getattr_calls = [call for call in dynamic_info['dynamic_calls'] if call.get('pattern') == 'getattr']
        if len(getattr_calls) > 5:
            recommendations.append({
                'type': 'performance',
                'priority': 'medium',
                'message': f"Found {len(getattr_calls)} getattr calls. Consider using direct attribute access "
                          "where possible or caching attribute lookups.",
                'affected_functions': list(set(call.get('function', 'unknown') for call in getattr_calls))
            })
        
        import_calls = dynamic_info['patterns']['module_import']
        if import_calls:
            recommendations.append({
                'type': 'maintainability',
                'priority': 'medium',
                'message': f"Found {len(import_calls)} dynamic import calls. Consider using static imports "
                          "or documenting the dynamic import requirements.",
                'affected_functions': list(set(call.get('function', 'unknown') for call in import_calls))
            })
        
        return recommendations
    
    def analyze_call_patterns(self, dynamic_calls: List[Dict[str, Any]]) -> Dict[str, Any]:
        """Analyze patterns in dynamic calls."""
        patterns = {
            'common_targets': defaultdict(int),
            'call_sequences': [],
            'potential_vulnerabilities': [],
            'optimization_opportunities': []
        }
        
        # Analyze common targets
        for call in dynamic_calls:
            if call.get('pattern') == 'getattr':
                # Try to extract the target object and attribute
                args = call.get('arguments', [])
                if len(args) >= 2:
                    obj = args[0]
                    attr = args[1]
                    patterns['common_targets'][f"{obj}.{attr}"] += 1
        
        # Look for potential vulnerabilities
        for call in dynamic_calls:
            if call.get('risk_level') == 'high':
                vulnerability = {
                    'type': 'code_execution',
                    'call': call,
                    'severity': 'high',
                    'description': f"Potentially unsafe {call.get('pattern')} call"
                }
                
                # Check if user input might be involved
                if self._might_involve_user_input(call):
                    vulnerability['severity'] = 'critical'
                    vulnerability['description'] += " with potential user input"
                
                patterns['potential_vulnerabilities'].append(vulnerability)
        
        return patterns
    
    def _might_involve_user_input(self, call: Dict[str, Any]) -> bool:
        """Check if a call might involve user input."""
        # Simple heuristics to detect potential user input
        suspicious_keywords = [
            'input', 'argv', 'request', 'form', 'query', 'param',
            'header', 'cookie', 'file', 'upload', 'post', 'get'
        ]
        
        # Check arguments for suspicious patterns
        args = call.get('arguments', [])
        for arg in args:
            if isinstance(arg, str):
                arg_lower = arg.lower()
                if any(keyword in arg_lower for keyword in suspicious_keywords):
                    return True
        
        return False


class DynamicCallVisitor(ast.NodeVisitor):
    """AST visitor for detecting dynamic calls."""
    
    def __init__(self, dynamic_patterns: Dict[str, Dict[str, Any]]):
        """Initialize the dynamic call visitor."""
        self.dynamic_patterns = dynamic_patterns
        self.dynamic_calls = []
        self.current_function = None
        self.current_class = None
    
    def visit_ClassDef(self, node: ast.ClassDef):
        """Visit class definition."""
        old_class = self.current_class
        self.current_class = node.name
        self.generic_visit(node)
        self.current_class = old_class
    
    def visit_FunctionDef(self, node: ast.FunctionDef):
        """Visit function definition."""
        old_function = self.current_function
        self.current_function = node.name
        self.generic_visit(node)
        self.current_function = old_function
    
    def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
        """Visit async function definition."""
        old_function = self.current_function
        self.current_function = node.name
        self.generic_visit(node)
        self.current_function = old_function
    
    def visit_Call(self, node: ast.Call):
        """Visit function call to detect dynamic patterns."""
        func_name = self._get_function_name(node.func)
        
        if func_name in self.dynamic_patterns:
            pattern_info = self.dynamic_patterns[func_name]
            
            call_info = {
                'pattern': func_name,
                'description': pattern_info['description'],
                'risk_level': pattern_info['risk_level'],
                'can_execute': pattern_info['can_execute'],
                'analysis_difficulty': pattern_info['analysis_difficulty'],
                'line': node.lineno,
                'col': node.col_offset,
                'function': self.current_function,
                'class': self.current_class,
                'arguments': self._extract_arguments(node),
                'call_type': self._categorize_call_type(func_name)
            }
            
            # Add specific analysis for certain patterns
            if func_name == 'getattr':
                call_info.update(self._analyze_getattr_call(node))
            elif func_name in ['eval', 'exec']:
                call_info.update(self._analyze_exec_call(node))
            elif func_name == '__import__':
                call_info.update(self._analyze_import_call(node))
            
            self.dynamic_calls.append(call_info)
        
        # Also check for indirect dynamic patterns
        self._check_indirect_patterns(node)
        
        self.generic_visit(node)
    
    def _get_function_name(self, func_node: ast.AST) -> str:
        """Get function name from call node."""
        if isinstance(func_node, ast.Name):
            return func_node.id
        elif isinstance(func_node, ast.Attribute):
            # For attribute calls like obj.method()
            return func_node.attr
        else:
            return ''
    
    def _extract_arguments(self, node: ast.Call) -> List[str]:
        """Extract string representations of call arguments."""
        args = []
        
        for arg in node.args:
            try:
                if isinstance(arg, ast.Constant):
                    args.append(repr(arg.value))
                else:
                    args.append(ast.unparse(arg))
            except:
                args.append('<complex_expression>')
        
        return args
    
    def _categorize_call_type(self, func_name: str) -> str:
        """Categorize the type of dynamic call."""
        if func_name in ['getattr', 'hasattr', 'setattr', 'delattr']:
            return 'attribute_access'
        elif func_name in ['eval', 'exec', 'compile']:
            return 'code_execution'
        elif func_name in ['__import__']:
            return 'module_import'
        elif func_name in ['globals', 'locals', 'vars']:
            return 'namespace_access'
        else:
            return 'other'
    
    def _analyze_getattr_call(self, node: ast.Call) -> Dict[str, Any]:
        """Analyze specific getattr call patterns."""
        analysis = {}
        
        if len(node.args) >= 2:
            # Try to extract object and attribute names
            obj_arg = node.args[0]
            attr_arg = node.args[1]
            
            # Check if attribute name is a constant
            if isinstance(attr_arg, ast.Constant) and isinstance(attr_arg.value, str):
                analysis['static_attribute'] = attr_arg.value
                analysis['predictable'] = True
            else:
                analysis['predictable'] = False
                analysis['dynamic_attribute'] = True
            
            # Check if there's a default value
            if len(node.args) >= 3:
                analysis['has_default'] = True
            else:
                analysis['has_default'] = False
            
            # Check if the result is called (indicating method call)
            analysis['likely_method_call'] = self._is_result_called(node)
        
        return analysis
    
    def _analyze_exec_call(self, node: ast.Call) -> Dict[str, Any]:
        """Analyze exec/eval call patterns."""
        analysis = {}
        
        if node.args:
            code_arg = node.args[0]
            
            # Check if code is a string literal
            if isinstance(code_arg, ast.Constant) and isinstance(code_arg.value, str):
                analysis['static_code'] = True
                analysis['code_preview'] = code_arg.value[:100] + '...' if len(code_arg.value) > 100 else code_arg.value
            else:
                analysis['dynamic_code'] = True
                analysis['high_risk'] = True
            
            # Check for global/local scope arguments
            if len(node.args) >= 2:
                analysis['custom_globals'] = True
            if len(node.args) >= 3:
                analysis['custom_locals'] = True
        
        return analysis
    
    def _analyze_import_call(self, node: ast.Call) -> Dict[str, Any]:
        """Analyze __import__ call patterns."""
        analysis = {}
        
        if node.args:
            module_arg = node.args[0]
            
            # Check if module name is static
            if isinstance(module_arg, ast.Constant) and isinstance(module_arg.value, str):
                analysis['static_module'] = module_arg.value
                analysis['predictable'] = True
            else:
                analysis['dynamic_module'] = True
                analysis['predictable'] = False
        
        return analysis
    
    def _is_result_called(self, node: ast.Call) -> bool:
        """Check if the result of a call is immediately called."""
        # This would require parent node information
        # For now, return False as a placeholder
        return False
    
    def _check_indirect_patterns(self, node: ast.Call):
        """Check for indirect dynamic patterns."""
        # Look for patterns like:
        # - Calling result of getattr: getattr(obj, 'method')()
        # - Using variables that might contain function names
        
        # Check if we're calling the result of getattr
        if (isinstance(node.func, ast.Call) and 
            isinstance(node.func.func, ast.Name) and 
            node.func.func.id == 'getattr'):
            
            self.dynamic_calls.append({
                'pattern': 'getattr_call',
                'description': 'Calling result of getattr (dynamic method call)',
                'risk_level': 'medium',
                'can_execute': True,
                'analysis_difficulty': 'high',
                'line': node.lineno,
                'col': node.col_offset,
                'function': self.current_function,
                'class': self.current_class,
                'call_type': 'attribute_access',
                'indirect': True
            })